Skip to content

Load-balance, active-box windowing, block-structured AMR - #1628

Open
sbryngelson wants to merge 694 commits into
MFlowCode:masterfrom
sbryngelson:up/mega
Open

Load-balance, active-box windowing, block-structured AMR#1628
sbryngelson wants to merge 694 commits into
MFlowCode:masterfrom
sbryngelson:up/mega

Conversation

@sbryngelson

@sbryngelson sbryngelson commented Jul 3, 2026

Copy link
Copy Markdown
Member

Summary

An opt-in, default-off family of performance features and the measurement infrastructure they rest on. With all flags at their defaults the only touched production path is s_mpi_decompose_computational_domain, refactored through the new m_box module (byte-identical; covered by the existing suite).

  • Load-balance infrastructurem_box (partition arithmetic), m_load_weight/load_weight_wrt (per-cell load-weight field + imbalance metric), m_sfc_partition/sfc_partition_wrt (Morton-SFC predicted-imbalance diagnostic), m_load_balance/load_balance (weighted static decomposition at init; AMR-fine-work-aware), m_rank_timing/rank_time_wrt (per-rank compute-time diagnostic).
  • Active-box windowingm_active_box/active_box: restricts reconstruction/Riemann/RK windows to a light-cone-grown box around non-ambient flow; strict-subset golden-tested.
  • Hybrid reconstruction/flux sensorshybrid_weno and hybrid_riemann (+hybrid_weno_eps, hybrid_smooth_flux): linear-optimal weights / central-or-Rusanov flux in smooth cells, full WENO/HLLC at flagged discontinuities (Jameson sensor, stencil-dilated, per-level under AMR).
  • Block-structured AMRm_amr + m_amr_registers: two-level 2:1 refined block hierarchy; conservative restriction and conservative-linear prolongation with physics-specific closures; per-stage flux registers with Berger–Colella refluxing; Berger–Rigoutsos multi-block dynamic regrid; optional dt/2 subcycling; multi-rank (single-owner blocks assigned by Morton-SFC work balancing at each regrid, with migration; blocks may span rank seams via P2P coarse↔fine gather/scatter; same-level seam halo; distributed registers); restart (both IO modes, regridded-layout persistence); AMR-aware post-processing (fine blocks visualizable as Silo overlay domains); GPU-resident fine level on both OpenACC and OpenMP offload.

Full algorithm and user documentation: docs/documentation/amr.md (support matrix enforced at runtime by the checker — unsupported combinations abort with named messages, never silently).

AMR physics support matrix (abridged; authoritative table in amr.md)

Supported and golden-tested: single- and multi-fluid (5-eq, mpp_lim) · 6-eq with per-block pressure relaxation · viscous (refluxed) · phase change (relax) · chemistry incl. species diffusion · Euler–Euler bubbles (polytropic/non-polytropic, mono/polydisperse, QBMM incl. non-polytropic with per-block pb/mv side-state; dynamic regrid + subcycle) · acoustic sources (coarse-grid support with regrid exclusion) · immersed boundaries (multi-body, static or prescribed-motion, incl. dynamic regrid with body-containment expansion and per-substage guards) · 2D axisymmetric (per-block WENO-coefficient recompute) · stretched grids (exact parent-bisection ghost coordinates + per-swap coefficient recompute) · hybrid WENO/Riemann sensors (per-level) · Lagrangian bubbles (cloud excluded from blocks; two-way coupling on the coarse grid; regrid clips around the moving cloud) · active_box (blocks contained in the growing window; agrees with plain AMR to ~1e-14) · IGR (restriction-only coupling: fine sigma solve seeded/Dirichlet-bounded by the coarse solve; documented truncation-order seam, exact free-stream) · 1D MHD/RMHD (div(B)=0 by construction in 1D; HLL and HLLD, incl. relativistic).

Gated with named aborts (documented rationale): surface tension (seam force imbalance is structural — three fixes attempted and diagnosed in amr.md) · 2D/3D MHD (attempted and measured: the c/f seam is a continuous O(1) div(B) source GLM cleaning cannot remove — needs constrained-transport-class B prolongation/reflux) · hyperelasticity · 3D cylindrical (global azimuthal filter) · force-driven IB (moving_ibm=2) · STL bodies · Riemann-extrapolation BCs (bc=-4) · amr_subcycle under IGR · stretched grids with Lagrangian/IB-regrid (uniform-spacing index conversions).

Validation evidence

  • Conservation: reflux-exact (~1e-15 defects) across single/multi-fluid, viscous, bubbles, chemistry, phase change, through subcycled + regridding advances.
  • Free-stream preservation: exact (0.0) with subcycling + regrid armed, including 3D, IGR, and Lagrangian configs.
  • Decomposition exactness: np=1 == np=2 element-exact with blocks spanning rank seams (fields byte-identical incl. chemistry species).
  • Methodology: every feature validated by AMR-vs-no-AMR reference comparison with a control experiment calibrating the expected error scale, plus mechanism evidence from run logs (regrid counts, box coordinates, ghost-point counts) — a golden that does not mechanically exercise its path is treated as no coverage. Liveness goldens for the hybrid sensors are deliberately sized so a silently dead sensor fails them (~5e-4 signal vs 5e-5 tolerance) — they caught a real dead-sensor bug on Cray GPU during CI hardening.
  • GPU: full test suite with post-process on 2×V100 — OpenACC 644/644 executed pass, OpenMP offload 643/644 (one slurm launch flake). Speed spot-check (800² 2D blast, 2 ranks): AMR + dynamic regrid costs +3% wall over no-AMR at these sizes; ACC ≈ 12% faster than OMP; acc/omp solutions bitwise identical.
  • CI: one fully green 85-lane matrix on record (all GitHub-hosted lanes: gnu/intel × debug/no-debug/reldebug × double/single/mixed, macos, and the NVHPC 23.11–26.3 compat matrix). Fixes landed during hardening were each reproduced locally in the exact failing configuration first (intel FP drift, single-precision tolerance classification, gfortran bounds-check OOB, OpenACC present-table mapping ×3).

Known issues (all non-gating or in progress)

  • Frontier/Cray, in progress: two device-mapping issues in the IB machinery under CCE (plain-IBM descriptor corruption on gpu-omp; a present-table crash in the AMR-IB swap on gpu-acc). Being fixed with direct Cray iteration; the CCE bench/case-opt lane failures share the same root.
  • NVHPC 24.1/24.3 compat lanes (informational, continue-on-error): an intermittent post-detected NaN on the two Lagrangian+AMR goldens. Exhaustively unreproducible off GitHub's runners — the exact failing stack (NVHPC 24.3 SDK, -tp=px -Kieee, HPC-X MPI, and the CI docker image itself under apptainer) passes elsewhere, as do native/zen2 builds; 24.5+ green. Documented at the golden definitions.
  • Accuracy postures documented rather than hidden: multi-fluid alpha-sum deviation bounded ~5.7e-3 at historical block faces during shock crossing (non-growing); viscous prolongation-ghost seam ~1e-6 np-dependence; IGR seam conservation is truncation-order (no reflux capture from the fused IGR kernels — exact-conservation capture is noted future work).

Review guide

The commit history is arc-ordered (active-box → load-weight → SFC → weighted decomposition → rank timing → hybrid → m_box → AMR rungs → physics envelope → CI/GPU hardening); reviewing by arc is much easier than by file. The AMR arc builds stepwise: static hierarchy → restriction/prolongation → fine advance → refluxing → regrid → subcycling → multi-rank → GPU → each physics rung with its own validation. Commit messages carry the validation evidence for their change (measured defects, golden UUIDs, repro details for CI fixes).

All parameters ship default-off with case_validator entries, runtime checker gates, and case.md/amr.md documentation.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces an opt-in (“default-off”) family of performance/diagnostic features (load-weight and SFC partition diagnostics, weighted init-time decomposition, rank timing), plus major simulation capabilities (active-box RHS windowing and block-structured AMR) and corresponding post-processing support and documentation/validation updates.

Changes:

  • Adds new runtime parameters and toolchain metadata/validation hooks for the experimental performance/AMR feature family.
  • Extends the simulation code with new modules for active-box restriction, load-weight diagnostics, SFC partition reporting, rank timing, and AMR integration points (including restart/output plumbing).
  • Updates post_process to read/write AMR fine-block overlays and adds/updates golden metadata plus documentation/indexing.

Reviewed changes

Copilot reviewed 82 out of 94 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
toolchain/mfc/params/descriptions.py Adds user-facing descriptions for new experimental/performance parameters.
toolchain/mfc/params/definitions.py Registers new parameters (AMR, hybrid sensors, load-balance diagnostics) and target applicability.
toolchain/mfc/lint_docs.py Treats new validator checks as non-physics doc checks.
tests/F980C769/golden-metadata.txt Adds golden metadata for a new/updated test.
tests/ECABA006/golden-metadata.txt Adds golden metadata for active-box test coverage.
tests/DD4CD8F3/golden-metadata.txt Adds golden metadata for a new/updated test.
tests/CC4213FD/golden-metadata.txt Adds golden metadata for a new/updated test.
tests/BD21A5C0/golden-metadata.txt Adds golden metadata for a new/updated test.
tests/BCBA6E74/golden-metadata.txt Adds golden metadata for a new/updated test.
tests/ACE05393/golden-metadata.txt Adds golden metadata for a new/updated test.
tests/987D9025/golden-metadata.txt Adds golden metadata for a new/updated test.
tests/852CCB81/golden-metadata.txt Adds golden metadata for AMR-related golden tests.
tests/65C375B4/golden-metadata.txt Adds golden metadata for a new/updated test.
tests/4DADE04B/golden-metadata.txt Adds golden metadata for a new/updated test.
tests/454C565F/golden-metadata.txt Adds golden metadata for a new/updated test.
tests/3A474BEE/golden-metadata.txt Adds golden metadata for a new/updated test.
tests/2FC423D3/golden-metadata.txt Adds golden metadata for a new/updated test.
tests/13945217/golden-metadata.txt Adds golden metadata for moving-IB under AMR test coverage.
src/simulation/m_viscous.fpp Clamps FD coefficient indexing to avoid ghost-region coefficient OOB in IB drag gradient evaluation.
src/simulation/m_time_steppers.fpp Integrates active-box bounds into RK update loops and interleaves AMR fine-stage/subcycle operations.
src/simulation/m_start_up.fpp Wires up new modules (rank timing, active-box, load-weight, SFC partition, AMR) into init/timestep/finalize and restart I/O.
src/simulation/m_sfc_partition.fpp Adds analysis-only SFC tiling + weighted partition prediction and reporting.
src/simulation/m_rank_timing.fpp Adds per-rank wall-time imbalance measurement helpers and reporting.
src/simulation/m_load_weight.fpp Adds per-cell load-weight field construction and rank-level imbalance reporting.
src/simulation/m_hypoelastic.fpp Refactors FD coefficient setup into a callable update routine (supporting AMR grid swaps).
src/simulation/m_global_parameters.fpp Adds AMR working-state mirrors and slot selection helper plus defaults for new parameters.
src/simulation/m_data_output.fpp Adds output/report hooks for load-weight, SFC partition, and rank-time diagnostics.
src/simulation/m_checker.fpp Adds input validation/prohibits for active-box, hybrid sensors, load-balance, and AMR configurations.
src/simulation/m_active_box.fpp Adds active-box initialization/growth and debug envelope checking.
src/simulation/m_acoustic_src.fpp Adds AMR-aware handling of acoustic source support (bounding boxes and overlap abort).
src/post_process/m_start_up.fpp Calls AMR fine-data reader and AMR overlay writer when amr is enabled.
src/post_process/m_global_parameters.fpp Adds default-off amr flag for post_process overlay behavior.
src/post_process/m_data_output.fpp Implements AMR fine-block overlay mesh/variables output (Silo/binary) and multimesh registration.
src/common/m_phase_change.fpp Exposes per-cell Newton iteration count and threads it through relaxation to support load-weighting.
src/common/m_global_parameters_common.fpp Adjusts start_idx lifecycle/allocation and makes load_weight_wrt visible to GPU macros.
src/common/m_derived_types.fpp Introduces a simple t_box type used by new partitioning infrastructure.
src/common/m_box.fpp Adds box/partition arithmetic helpers (equal/weighted splits, box-from-splits).
src/common/m_boundary_common.fpp Skips BC buffer population during AMR fine advance to rely on coarse-driven ghost fill.
docs/module_categories.json Registers new modules under documentation categories.
docs/documentation/readme.md Adds AMR section link to the documentation index.
.typos.toml Adds project-specific abbreviations to the spelling allowlist.

Comment thread src/common/m_phase_change.fpp Outdated
D = ((gs_min(lp) - 1.0_wp)*cvs(lp))/((gs_min(vp) - 1.0_wp)*cvs(vp))

#ifdef MFC_SIMULATION
if (relax .and. load_weight_wrt) then
Comment thread src/simulation/m_sfc_partition.fpp
Comment thread src/simulation/m_sfc_partition.fpp Outdated
Comment thread src/simulation/m_rank_timing.fpp
@codecov

codecov Bot commented Jul 6, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 57.71812% with 945 lines in your changes missing coverage. Please review.
✅ Project coverage is 61.59%. Comparing base (41df185) to head (007dd8c).
⚠️ Report is 2 commits behind head on master.

Files with missing lines Patch % Lines
src/simulation/m_amr_registers.fpp 66.66% 93 Missing and 102 partials ⚠️
src/post_process/m_data_input.f90 2.54% 152 Missing and 1 partial ⚠️
src/post_process/m_data_output.fpp 0.00% 96 Missing ⚠️
src/simulation/m_amr_restart.fpp 61.18% 57 Missing and 28 partials ⚠️
src/simulation/m_rhs.fpp 66.85% 46 Missing and 12 partials ⚠️
src/simulation/m_load_balance.fpp 46.80% 42 Missing and 8 partials ⚠️
src/simulation/m_ibm.fpp 71.54% 19 Missing and 16 partials ⚠️
src/simulation/m_load_weight.fpp 21.05% 23 Missing and 7 partials ⚠️
src/simulation/m_sfc_partition.fpp 62.50% 23 Missing and 7 partials ⚠️
src/common/m_mpi_common.fpp 47.82% 13 Missing and 11 partials ⚠️
... and 26 more
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1628      +/-   ##
==========================================
- Coverage   61.67%   61.59%   -0.09%     
==========================================
  Files          84       96      +12     
  Lines       21619    28741    +7122     
  Branches     3196     4705    +1509     
==========================================
+ Hits        13334    17702    +4368     
- Misses       6093     7909    +1816     
- Partials     2192     3130     +938     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@sbryngelson

Copy link
Copy Markdown
Member Author

Upstream latent gap found during the MHD+AMR investigation (independent of this PR): m_riemann_solver_hlld.fpp contains no GLM terms, so hyper_cleaning = T with riemann_solver = 4 (HLLD) is silently inert — psi never evolves and no cleaning occurs. The existing hyper_cleaning golden uses HLL, which does carry the GLM coupling, so CI never sees this. Worth either adding GLM terms to HLLD or a validator prohibit on the combination.

Also for the record: MHD+AMR was attempted and re-gated on measured evidence rather than assumption — the coarse/fine seam is a continuous O(1) div(B) source that cleaning spreads but cannot remove (details in the amr.md support matrix row and commit ac203b1).

@sbryngelson sbryngelson changed the title Experimental performance family (default-off): load-balance infrastructure, active-box windowing, block-structured AMR, hybrid WENO/Riemann sensors Load-balance, active-box windowing, block-structured AMR Jul 21, 2026
@sbryngelson

Copy link
Copy Markdown
Member Author

active_box is single-rank-only — make it MPI-real, or defer it?

Raising this as a self-review point. active_box disables itself with a warning at num_procs > 1 and falls back to full-domain compute. Since production runs here are essentially always multi-rank, that makes the optimization a no-op (plus a warning line and some maintenance surface) in exactly the configurations that matter. We should decide what to do with it.

Why it's single-rank today. The active box is a single global, coarse-cell-indexed region. On one rank you just clip the RHS loops (convert / reconstruct / Riemann) to it. Under domain decomposition the geometry is easy — each rank intersects the global box with its subdomain and freezes the exterior — but it creates a load-imbalance problem: if the disturbance occupies only a few ranks' territory, those ranks do all the work while the rest idle, and the slowest rank sets wall-clock, so you save nothing. The current code sidesteps this by disabling rather than solving it.

The enabling pieces are already in this PR. A multi-rank active box needs (a) per-rank clipping of the box (straightforward), and (b) a way to keep otherwise-idle ranks busy — i.e. redistribute the domain so the active region spreads across ranks. That second part is exactly what this PR's load-balance arc (m_load_balance / m_sfc_partition / m_load_weight) provides, and the active box already produces the natural work-weight signal (active vs. frozen cells) that a rebalancer would consume. The two features are built to compose — active box marks where the work is, load-balance moves ranks to it — they're just not wired together yet.

Options:

  1. Make it MPI-real. Per-rank box clipping + feed the active/frozen cell count into the load-weight so load_balance redistributes toward the active region. This is the version that actually helps at scale, and it's the natural payoff of shipping the load-balance infrastructure in the same PR.
  2. Defer / gate it. Drop active_box from this PR (or keep it behind an explicit experimental flag with the single-rank limitation documented up front) and keep the performance family focused on AMR, which is already multi-rank-capable via the P2P device-path gather/scatter. Re-introduce active box once the load-balance integration is designed.

Leaning toward (1) if there's appetite, since the enabling infra is right here; otherwise (2) keeps the PR's surface honest — a default-off single-node optimization advertised in an exascale code invites confusion. At minimum, the docs should state plainly that it is single-rank-only today.

sbryngelson and others added 16 commits July 30, 2026 17:25
s_amr_advance_children drove one parent's whole subtree to completion before the next parent's, but the s_amr_fine_fine_halo(clev) interposed in its stage loop exchanges EVERY level-clev seam pair. A pair straddling two parents therefore had only one side present, and the side that was present read neighbour data from before the other parent's children were even set up. Tower co-location hid the MPI half (both ends of such a pair landed on one rank, so the exchange was a local device copy), which is why multi-level subcycling is checker-gated at np>1.

Take a LEVEL rather than a parent slot and walk every block at that level together, looking each block's parent up per iteration. The level-1 driver's per-parent loop collapses to a single unconditional call, which also lets a rank owning a descendant but not the parent reach the fold.

At np=1 this only re-orders independent per-parent work - each child reads solely its own parent's finished snapshots and its same-level neighbours - so results are unchanged.

Precheck 7/7, 70/70 AMR goldens.
Step 4 of docs/documentation/amr_per_level_distribution.md. amr_fine_cut becomes one independent SFC cut per level, so every level is balanced across all ranks on its own weight; the twt tower rollup, the descendant-inherit pass, and f_amr_owner's level-1 anchor walk are deleted. A deep refinement tower no longer pins its whole subtree (weight cost*rr**(l*d)) to one rank, which was the granularity cap at depth.

Per level rather than one mixed cut over all fine blocks: same-level boxes are disjoint and so carry distinct Morton keys, which the cut-point binary search needs. Mixed, a level-2 block sharing its parent's region_lo would collide with it.

Splitting towers made four latent reads reachable, all of the same shape - a parent-slot field guarded on owning the CHILD, safe only while the two owners always coincided. s_set_amr_fine_geometry read amr_slots(pblk)%amr_ref_ratio (written by s_amr_alloc_slot, never called for pblk on a rank that does not own it, so undefined) and bisected amr_slots(pblk)%x_cb (unallocated there); the ghost-shell extension took lbound/ubound of the same unallocated array. Together they produced garbage cell widths and NaNs at the rank seam a few steps in - EF58E377, the one golden whose level-2 tiles actually divide across ranks.

Footprint and refinement ratio now come from s_amr_parent_foot, and coordinates from a new s_amr_build_block_coords that replays the ancestor chain from the global L0 boundaries. Refinement is nested midpoint subdivision, so the replay reproduces exactly what bisecting the parent's stored array gave, and it collapses the level-1 and level>=2 coordinate branches into one call. The subcycle SETUP gather gets the same participant split the runtime gather already had.

Precheck 7/7, 70/70 AMR goldens - the first run with a genuinely split tower, so the cross-rank gather/restrict/reflux are exercised rather than dormant.
…asured outcome

The plan doc named load balancing once, only to reject an alternative - it had no mechanism, no metric, and no acceptance criteria, while the goal of the work is AMR *plus* load balancing.

Adds a Load balancing section: the three mechanisms often conflated (L0 Cartesian rebalance, the cost model / imbalance diagnostic, and the AMR block assignment), the max/mean per-rank-weight metric with scale invariance as the requirement that actually matters, and the four structural limits that bind at scale - granularity floor, indivisible atom, staleness within a regrid interval, and cost-model blindness when load_weight_wrt is off. Notes that step 4 is the load-balancing enabler rather than only a distribution change: no assignment can fix an imbalance whose atom is a whole tower.

Records the step-4 A/B honestly. Per-level distribution measured FLAT against co-location at np=1/2/4/8, and the result is not attributable because there is no imbalance metric - a flat end-to-end time cannot separate 'nothing to fix' from 'balancer did nothing'. Instrumentation therefore becomes step 5 and precedes further distribution work. Also records that the same case costs ~20x the coarse solve while its refinement accounts for ~2.5x, putting the dominant term in per-block launch overhead rather than balance.

Marks steps 1-4 landed with commits, rewrites the stale Track 2 section, adds the parent-slot-guarded-on-the-child bug class that splitting towers exposed, and corrects the validation bar: cross-compiler coverage is CI's job, the local gate is amdflang OMP offload.
…s inconclusive

The uniform control reaches only 18% parallel efficiency at np=8 (262k cells/rank, below what saturates an MI250X GCD), so both arms are latency-bound exactly where per-level distribution should matter most. The flat A/B therefore could not have detected the effect and is inconclusive, not negative. Adds the efficiency table and a standing rule: read the control's efficiency before trusting anything measured against it.

Also corrects the overhead figure. It was quoted at np=8, where the uniform number is already degraded; at np=1, where both arms are compute-bound, AMR costs 40.7x the coarse solve for ~2.5x the nominal work - a ~16x residual, not ~8x.
The step-4 A/B measured flat at every rank count and could not be interpreted: with no imbalance metric a flat time cannot separate 'no imbalance to fix' from 'balancer did nothing' from 'gain offset by new P2P traffic'. Print the quantity the SFC cut is actually minimising - max_r W(r) / mean_r W(r) per level and in total - plus the count of ranks assigned nothing, which is the granularity floor showing up directly. Needs no MPI: wt, amr_block_level and amr_block_owner are replicated and identical on every rank (the cost vector is allreduced in s_amr_block_cost), so rank 0 prints. Gated behind load_weight_wrt, so every golden is unaffected. Precheck 7/7, 70/70 AMR goldens.
Audit of fc53e09 found two flaws in the report itself. (1) merge(mx/mean, 1._wp, mean > 0._wp) does not guard: merge is a function, so both arms are evaluated and the mean == 0 arm still divides by zero. Use a real if. (2) rw/tw were automatic arrays dimensioned 0:num_procs-1 - O(num_procs) on the STACK, in the one routine whose purpose is measuring behavior at large rank counts; the module already heap-allocates amr_owner_cut and amr_fine_cut at exactly that size. Allocate them. Also rename idle_ranks -> no_blocks_ranks / ranks_with_no_fine_block: the counter is per-LEVEL, and a rank with no block at a level still owns level-0 work, so the old name invited reading it as idleness. It is not; only m_rank_timing measures that. Separately, the s_amr_advance_fine_subcycle_all docstring claimed the L2-L2 seam halo was future work and that s_amr_check_seam_topology aborts on L2+ seams under subcycle - 3db24df removed that abort and the halo exists; and the s_amr_tile_box tsz docstring named a fixed amr_maxc_fit/2. Precheck 7/7, 71/71 AMR goldens.
The regrid child-box cap ran inside do lev = 2, amr_max_level but passed amr_maxc_fit/2 at every level. That constant is derived for level 2 specifically: a slot holds amr_ref_ratio*amr_maxc_fit fine cells (max_f1 = amr_ref_ratio*amr_maxc_fit - 1) and a level-lev block spans amr_ref_ratio**lev fine cells per coarse cell, so the cap must be amr_maxc_fit/amr_ref_ratio**(lev-1). At lev = 3 the fixed /2 admits a box twice what the slot holds. Identical at lev = 2 (amr_ref_ratio is 2 whenever amr_max_level > 1 - the checker gates rr /= 2 to single-level), so no existing golden moves, and 71/71 AMR goldens confirm that. NOT VERIFIED BY RUNNING: no golden covers amr_max_level >= 3, so this is arithmetic checked against the slot sizing, not observed behavior. It is strictly more correct than the constant it replaces and cannot regress level 2, but do not read the green suite as evidence that level 3 works. No call-site floor is needed - s_amr_tile_box already does tc = max(tc, 1) for exactly the zero case (collapsed dims pass 0 today).
The PROHIBIT's stated reason - that s_amr_advance_children advances level-2 children per-block with no L2-L2 halo - stopped being true at 3db24df, which removed the matching runtime abort saying 'the halo it warned about now exists'. 26e0d08 then extended the transpose across parents. Blast radius is narrow: every risky combination keeps its own gate (non-polytropic QBMM np>1, cyl_coord, IB np>1, moving IB, amr_ref_ratio /= 2, amr_max_blocks < 2), so this opens only plain Cartesian hydro at rr=2. No test expected the abort. New golden C45DBB52 covers it, VERIFIED by dumping block lo/hi/owner rather than assumed: adjacent L2 siblings [7,11]+[12,16] under one parent, and a child on a different rank from its parent (L2 [52,56] rank 0 under L1 [49,59] rank 1) at an intermediate regrid - the per-substep P2P SETUP gather, whose failure mode is a deadlock no tolerance check catches. NOT covered, deliberately: the L2 seam over MPI. Morton order keeps a contiguous run contiguous and the SFC cut is a single split point, so the adjacent pair lands on one rank; an eps x buf sweep (0.002-0.05 x 8,12) never split it, and contriving one would pin the golden to an SFC outcome any cost-model change would undo. Both ingredients are covered separately (level-2 fmul by A635AA56 at np=1, MPI seam transport by the single-level subcycle np=2 golden). Note cross-parent seams are impossible at any depth: the nesting window insets each parent by amr_cpat_mar >= 1. Precheck 7/7, 71/71 AMR goldens.
The granularity-floor section said a shallow hierarchy leaves ranks 'idle by construction', and I read the step-5 np=8 measurement the same way, reporting 'idle_ranks 6 of 8' as 75 percent of the machine idle. That is wrong. The counter is per-LEVEL: a rank holding no level-L block still owns level-0 work (level 0 covers every rank) and may hold blocks at other levels; even the TOTAL line means only 'owns no fine block'. So the number does not explain the 18 percent parallel efficiency - that still needs measured per-rank time, which is the point the metric section already makes. Also mark step 5 landed while stating that its m_rank_timing half, the half that measures idleness rather than inferring it, is not done.
Removing the multi-level subcycle PROHIBIT left twelve lines of explanation sitting in a list of checks with nothing after them - a comment justifying why something is NOT checked, in the file whose job is checking. The content is already in the two places it belongs: s_amr_advance_children's docstring (the level-wide walk and the interposed halo) and the C45DBB52 test comment (the amr_cpat_mar margin argument).
be94db3 shipped the per-level cap saying it was verified by reading the slot arithmetic, not by running, because no golden covers amr_max_level >= 3. It is now verified by running, and the evidence belongs where the next reader will be standing. m = 255, amr_max_level = 3, amr_buf = 48, np = 1: the fixed /2 keeps ONE oversized level-3 box where the per-level cap keeps 2, and the run aborts in s_amr_free_slot with a Fortran 'Invalid descriptor' and a core dump; the per-level cap exits 0. Two earlier counterfactuals produced byte-identical output and proved nothing - m = 63 was too small, and scaling the grid without scaling amr_buf left the boxes tracking the feature rather than the domain, far below either cap. A635AA56's comment already says it is the BUFFER that widens the box; applying that is what made the cap bind.
…nchmarking

The balancer only becomes observable when a level holds many boxes, and stripe cases collapse to ~2 boxes per level by np=8 - at which point every owner mapping produces an identical assignment. Scattered blobs give many disjoint tag clusters, hence many boxes per level. Hardcoded rather than an analytic patch on purpose: analytic expressions compile into case.fpp, so every variation costs a rebuild, whereas a(2:5) (count, seed, radius, amplitude) are read at run time and sweep freely against one binary. Centres come from an additive-irrational (Weyl) sequence, NOT random_number: each rank fills only its own cells, so the IC must be a pure function of position or ranks disagree at the subdomain seam and the decomposition stops being exact. Purely additive - a new case in a select, unreachable unless a case sets hcid = 299 - so no existing golden can enter it. Verified: all four targets build (the file compiles into pre_process via m_icpp_patches and simulation via m_ib_patches), pre_process runs it clean at 511x255 on 2 ranks. Also revises the plan doc: step 5 landed, acceptance criterion 2 promoted to a runnable step, and a retraction of this file's own earlier claim that m_rank_timing was unimplemented - it is fully wired at eight call sites.
…uption)

s_amr_regrid's 'brand-new region' branch - a level>=2 child whose region has no old fine data to cluster - emitted its box straight into boxes() without ever consulting amr_maxc_fit. It is the ONLY box-emitting path that skips s_amr_tile_box. The inset bounds the child as a FRACTION of its parent (span/4 each side), but the slot coord arrays are allocated ONCE to amr_ref_ratio*amr_maxc_fit, so the child must be bounded in ABSOLUTE cells. s_amr_build_block_coords then sizes fcb from the block's TRUE extent and writes past x_cb. Under -O2 that silently scribbles past a heap allocation every regrid and surfaces much later as 'corrupted size vs. prev_size' inside an unrelated free() - the crash site is nowhere near the bug site, which is why three passes of reading the seam/overlap/tiling code cleared every candidate. Integer division is the trigger: s_amr_tile_box splits a wide region into 63 and 64, not 64 and 64, and a parent of span 63 gives ins = 63/4 = 15 and a child of span 33 against a level-2 cap of 32, while span 64 gives exactly 32 and is fine. A one-cell difference in the parent flips it. Introduced by me in cfdd284 (step 4): s_amr_build_block_coords replaced a parent-relative computation with a global-extent one so a rank could build coords for a block whose parent it does not own, and the destination sizing did not follow. VERIFIED: bounds-checked CPU build (gfortran --no-gpu --debug, the ONLY configuration with -fcheck - cmake/GPU.cmake gives LLVMFlang none) traps at m_amr.fpp:584 'Index 128 of dimension 1 of array fcb above upper bound of 127' without this change, and with it the span-33 box becomes 17+16 and the run exits 0.
amr_max_grid_size had ZERO test coverage - not one golden or example case set it, only the schema, validator and definitions - despite being the mechanism the rank-independent-cap work rests on. Any defect reachable only through the pinned-cap path was invisible to CI, and one was: the brand-new-region box emitter skipped s_amr_tile_box and corrupted the heap (cfbaceb). VERIFIED to fail without that fix: 8 over-cap boxes and, under a bounds-checked build, 'Index 32 of dimension 1 of array fcb above upper bound of 31' (the trap index tracks the cap, max_f1 = 2*amr_max_grid_size - 1). Both knobs are load-bearing and narrow. amr_max_grid_size = 16 makes s_amr_tile_box split a wide region into 16 and 15, and span 15 gives ins = 15/4 = 3 and a child of 9 against a level-2 cap of 8; INTEGER DIVISION is the trigger, so a cap of 20 splits evenly (20/20 -> child 10, cap 10) and does not reproduce. The blob IC is equally load-bearing - a Sod-like patch grows level-2 regions that already have fine data, so the brand-new-region branch never fires and the bug is unreachable however the cap is set; three separate small 1D Sod candidates passed for exactly that reason. Also records the caveat in the plan doc: step 2's measured speedup was collected on this path, so it is no longer a clean measurement. 72/72 AMR goldens, precheck 7/7.
Two additions, both from today's heap-corruption hunt (cfbaceb). (1) s_amr_check_box_caps aborts if any level>=1 box exceeds amr_maxc_fit/amr_ref_ratio**(lev-1), checked once where the box set is final rather than trusting each emitter to have routed through s_amr_tile_box. A violation is otherwise silent and catastrophic - the coord arrays are sized to the cap, so an over-cap box writes past x_cb and corrupts the heap every regrid, surfacing much later inside an unrelated free(). VERIFIED BOTH WAYS: on the unfixed reproducer it prints '[amr] box cap violated: level 2 dim 1 span 9 > cap 8' and aborts by name instead of corrupting memory; and 71/71 existing AMR cases pass with it compiled in, zero false aborts. The false-positive hazard was real - in 1D/2D amr_maxc_fit(2:3) is 1, so the level-2 cap divides to 0 and a naive check would abort every 1D golden; it mirrors s_amr_tile_box's own max(tc,1) floor. (2) [amr-balance] now prints boxes_max/mean beside the weight imbalance, because measured per-rank time tracks BOX COUNT rather than cells: at np=8 the cell weight sat at 1.050 while box count was 1.308 and rank_time 1.259. Printing both makes that falsifiable in one run. 72/72 AMR goldens, precheck 7/7.
s_amr_sfc_cut advances a rank when cum >= tgt, where cum is an n-term ACCUMULATION and tgt = (r+1)*total/num_procs is CLOSED FORM over another n-term sum. At an exact share boundary the two differ by rounding rather than by intent, so the comparison turns on 1 ULP. This has been correct only by luck: every cost term to date is integer-valued (footprint cells, K_ib = 2, K_pc = 3 x integer iteration counts), so 4w and 32w/8 agree bit for bit. ANY fractional cost term breaks it, silently - ranks come out unbalanced with no error and no diagnostic. MEASURED with a fractional term in cost(k): 32 IDENTICAL level-1 weights (9.038222222E+03, verified by printing them) over 8 ranks split 5/3 instead of 4/4, reporting max/mean 1.250 where the same case with integer weights reports exactly 1.000. Adding a tolerance of O(n) ULP of the target - the accumulated rounding bound - restores 1.000 while leaving the genuine imbalance at the other level untouched (1.106, unchanged to three decimals). Far from a share boundary the tolerance is ~1e-12 relative and the greedy is unchanged, which is what 72/72 AMR goldens confirm: no existing case moves. Found while adding a per-box cost term; that term is NOT in this commit - the fragility predates it and is worth fixing on its own.
fahnab666 added a commit to fahnab666/mfc-nabid that referenced this pull request Aug 29, 2026
…isting lint debt

Merges the 20 commits added to sbryngelson/up-mega since ee7758b: the CCE descriptor and GPU_DECLARE fixes for the Frontier AMR abort, the NVHPC declare-ordering fix, restart format v2 (per-block owner/extents), amr_blocking_factor defaulting to 4, and the level-order clustering reductions. The delta is confined to AMR: m_amr, m_amr_regrid, m_amr_restart, m_constants, m_global_parameters, cmake/GPU.cmake, 14 AMR goldens, and two AMR-only toolchain hunks. No JWL, IBM, Riemann, or EOS source file is touched and no non-AMR golden changes. Merge was conflict-free; the local static-AMR GPU allocation cap for IBM survives intact.

Also clears lint debt that predates this merge and blocked any commit: formatter line-wrapping in six JWL/IBM files, the duplicated rxn_val PROHIBITs in pre_process/m_checker.fpp (already enforced in case_validator.check_stiffened_eos, as its own docstring states), the stale hyperelasticity row in case.md (removed upstream), the missing sec-mhd anchor, and m_jwl/m_jwl_sources missing from module_categories.json.
…ntier CCE gpu-acc)

A declare-create on a module allocatable binds a present-table entry to the descriptor at program init; the move_alloc at the allocation site then swaps that descriptor out, so every later kernel lookup misses. All 36 AMR tests on the CCE gpu-acc lane died with 'find_in_present_table failed' for amr_cons_br (432 hits) and amr_scr_prim (9). amr_cg has always used move_alloc + GPU_ENTER_DATA with no declare and works on every lane; this makes the other two match. The declares were added for the lib-4425 uninitialized-descriptor abort, but the move_alloc is what actually fixed that, so they were redundant as well as harmful. amdflang's OpenMP runtime creates the mapping implicitly, which is why no local gate could see this.
s_populate_variables_buffers sits inside s_compute_rhs, which PH_COARSE wraps whole, so a uniform (amr=F) run reported 95% 'coarse' and no communication at all. That made the one comparison that matters impossible: how much of AMR's ~31% communication share is AMR's own versus the solver's baseline.
Halo probe counts distinct block slots this rank's metadata reads touch between regrids, reduced with MPI_MAX over ranks rather than reported from rank 0 (which owns a domain corner and is the least connected rank). The reduction is entered by EVERY rank: an earlier draft placed it inside the proc_rank == 0 guard, so one rank called ALLREDUCE while the rest ran ahead into other collectives and the job died with MPI_ERR_TRUNCATE. Grid efficiency reports tagged versus covered coarse cells, which prices amr_tag_eps and amr_buf and bounds the quoted payoff.
…reshold

A histogram of the tagging ratio g/(2*r0) over ~49 regrids of this case shows the distribution is continuous from 0 to 0.5 with no gap, and amr_tag_eps = 0.05 already sits at a local minimum, so there is nowhere better to move it. Near the threshold the density is ~9,400 cells per unit ratio per regrid, so one flip across 49 regrids needs a perturbation of ~2e-6; roundoff is 1e-12 to 1e-14 and cannot reach that. The flipped tag is therefore a consequence of divergence, not its cause: roundoff is amplified by a chaotic configuration until it reaches ~1e-6, and only then do tags flip. t_step_stop 100 -> 25 keeps 12 regrids and the store still grows 25 -> 36 -> 49 blocks with the cap never binding, while giving roundoff far less room to grow. override_tol = 1e-11 covers the residual toolchain roundoff (CCE measured 1.06e-12) without being loose enough to hide a mesh divergence.
The allocation is guarded by hypo_nc_mode == hypo_nc_mode_dual_pass alone, but the teardown sat nested inside if (.not. igr), so any configuration with igr and dual_pass together leaked flux_n, flux_src_n and flux_gsrc_n along with their %sf payloads and device maps. Nothing couples igr to hypo_nc_mode in the validator, so the combination is reachable. Hoisted the teardown block out of the igr guard so the two are textually identical.
Nothing in the tree writes this array, but s_finalize_riemann_solver_hatR copies all of 1:sys_size out of it into flux_gsrc_n(id), which m_rhs folds into the RHS. Under cyl_coord with the dual-pass path that fed uninitialized memory into the solution. Mirrors the zeroing its twin flux_gsrc_rsx_vf already gets for the same reason.
s_compute_load_weight reads it on the first s_write_data_files, which for a run saving at t_step_start precedes any relaxation sweep, so the first load-weight diagnostic and load_weight.*.dat came from uninitialized memory. The device copy is the one that matters: the reader is a GPU_PARALLEL_LOOP and the only writer sits inside a device region, so a host-only assignment would have reached neither.
s_write_amr_restart marks format v2 with a NEGATIVE rank count, and the simulation reader was taught both formats, but the post_process reader was not. Its rank-count check therefore fires unconditionally (a negative can never equal num_procs) and reports a rank-count mismatch even when the counts match, which sends the reader after the wrong problem; it also still reads the 3*num_procs extents record that v2 does not write. Detect the negative marker in both the serial and MPI branches and abort saying what is actually wrong. This is a stopgap: reading v2 in post needs the whole-block ownership layout (one contiguous chunk per block, ownership from the file's 4-int record rather than derived geometrically), which is a separate change. Nothing in the default suite runs AMR with post_process as a target, which is why the break shipped unnoticed.
Only the parallel_io branch of s_write_amr_restart writes the negative rank count that marks format v2; the serial branch still writes a positive num_procs to p_all/p<rank>/<t_step>/amr_fine.dat. The guard added to the serial reader could therefore never fire, and its comment claimed a failure mode that path does not have. Verified against a real 8-rank AMR run: the parallel file's header is (-8, 224, 6), so the parallel guard is the one that matters.
The simulation path raises MFCException on a non-zero return code; the --test-all post_process path captured stdout and ignored the result entirely (common.system runs with check=False). post_process could abort, segfault or fail outright and the test still reported PASS as long as the simulation goldens matched. That is how a total break of the AMR post-process reader shipped unnoticed: --test-all ran post_process on every AMR case and threw the answer away. Verified both directions: an AMR test now fails where it previously passed, and non-AMR cases are unaffected.
Two defects, both in the AMR fine-block reader, both invisible until the test suite started checking post_process's exit code.

First: format v2 was never taught to post. The writer marks v2 with a negative rank count, the simulation reader handles both formats, and post did not, so its rank-count check fired unconditionally (a negative can never equal num_procs) and blamed a rank-count mismatch on runs that used the right number of ranks. v2 stores one contiguous chunk per block written by that block's single owner, plus a 4-int (owner + 1, m, n, p) record. Ownership now comes from that record rather than from the reader's own geometric intersection, which also fixes a case where a rank overlapping a block it does not own would read a -1 extent and abort. The whole chunk is read and this rank's intersection sub-box copied out, so s_setup_amr_block still reconstructs coordinates from a valid local coarse index. v1 files stay readable.

Second: post runs with a LARGER sys_size than the simulation for 5eq Lagrange bubbles, because it appends beta_idx as a post-only output slot. Comparing the file's conserved-variable count against post's inflated sys_size rejected every valid AMR + Lagrange-bubbles file. Size and read by the count the writer actually used.
[amr-grideff] printed amr_n_tagged, which counts THIS rank's local tag_grid, next to amr_n_covered, which comes from the global accepted box list (s_amr_cluster runs with reduce = .true., so every rank clusters the same global tag set and already holds the same volume). Printing the unreduced pair from rank 0 made the ratio wrong by about num_procs. Sum the numerator across ranks and leave the denominator alone. Every rank enters the collective; only rank 0 prints. Still approximate and marked as such in the source: the numerator mixes the level-1 and level-2 index spaces, and it is accumulated before the amr_buf pad and the box merge, so real over-coverage is worse than the printed ratio.
With l0_ntile > 0 the L0 tiles occupy slots 1..l0_slot_off of the SAME pool as the fine blocks and amr_num_blocks counts them, so s_write_amr_restart emits level-0 tile records into the AMR fine-block file. All three reader paths rejected them twice over: lvl == 0 fails the lvl < 1 test, and a tile's refinement ratio of 1 fails rr < 2. Every AMR + L0-tiles case therefore died in post_process with 'malformed fine-block header (level/extent inconsistent); the writer and reader header layouts have drifted' -- a message that blamed a drift which had not happened. A tile's data is the base grid re-tiled and is already in the level-0 restart file, so the overlay skips it while still advancing the file offset past the record. Reproduced and fixed on a CPU build, which is where CI sees it: all nine cases fail with the old reader and pass with the new one. They pass on amdflang GPU either way, so no local GPU gate could have caught this.
amr_n_shaped counts the coarse volume the FINAL boxes occupy, after s_amr_regrid_shape_boxes has padded by amr_buf, clamped, size-capped, clipped and tiled. amr_n_covered is taken before all of that, so it cannot see the pad, which is exactly where amr_buf acts: across an amr_buf 4 -> 1 sweep covered moved 0.4% while the refined volume moved 9%. [amr-grideff] now prints tagged, covered and shaped together. Also warn when the global accepted-box count exceeds amr_max_blocks: boxes contributed by other ranks were discarded silently, so the refined mesh stopped matching the tag field with no diagnostic. The existing capped flag covers only the tree bisection, not this truncation.
The runtime banner and the per-level distribution table both asserted 1.55x as a measured subcycle speedup. It is a phase-share model and was never measured. One matched-resolution arm pair (equal physical time, level 2 at the same dt in both) gave 2.84x, but that T arm's phase table omits rhs, seam, reflux, gather and rk entirely because the subcycle advance path carries no PH_* brackets, so it is a wall ratio with no accounting behind it. Both figures are now labelled for what they are. Separately, m_time_steppers said multi-level reflux under subcycling is future work; s_amr_advance_children has been calling s_amr_reflux_to_parent(dt_sub, .true.) for some time.
Records what the reviews established: the np8-512 weak-scaling ladder already existed and had never been analysed (rg:clus is O(P^3) and 75% of wall at np=512; rb:topo is exactly O(P^2)); the emitted box set is a single bounding box re-tiled, so amr_cluster_eff cannot affect it and the measured 2.20x over-coverage is this clusterer's best case on its most flattering geometry; and the per-step global scan work this plan had as item 1 is 2.5% of wall at np=512, measured, against a P^2 and a P^3 term beside it.
Under cfl_dt the simulation names each save by save_count = int(mytime/t_save) (m_start_up.fpp:798), so when adaptive dt grows enough for one step to cross TWO t_save boundaries the index skips and no file is written for the intervening value. The post loop walks indices 0..n_save-1 and the reader aborts on the first absent one, so every CFL-driven case died in post_process with 'File restart_data/lustre_N.dat is missing'. The IGR_triple_point case writes 60 saves at indices 0, 1, 3, 5, 7 and upward, and post demanded index 2. A skipped index is legitimate output, not a fault. f_save_exists lives beside the reader that owns the filename convention and only answers for the shared-file layout, since with file_per_process each rank holds a different file and the answer would not be rank-uniform; that path keeps the original fail-closed behaviour. Reproduced and fixed on a CPU build: the case fails before and passes after, and 12 non-CFL cases still pass.
…FlowCode#1795

4C751DAF aborts on the Frontier CCE cpu lane with 'No Lagrangian bubbles remain in the domain', and passes on amdflang GPU and gfortran CPU. Measured on gfortran over the full run, from the case's own lag_bubble_evol output: the single bubble moves 0.5 -> 0.5000076, its radius goes 0.008 -> 0.0079987 (it shrinks), and the void fraction in its cell peaks at 0.0335 against a valmaxvoid threshold of 0.99 -- a 3.1x margin. Neither removal criterion is reachable, so the count reaching zero on CCE is a symptom of the bubble state degrading there, not a physical outcome. The case is therefore NOT loosened to dodge the abort: with that much margin on working toolchains, any such change would hide whatever actually goes wrong on CCE. Only the --test-all re-run trips it, so the primary run and the golden comparison still cover the case fully. This is present on master too, invisible because the suite discarded the --test-all exit code.
@github-actions

Copy link
Copy Markdown

Lines of Code

File Lines Diff
src/simulation/m_amr.fpp 7133 +7133
src/simulation/m_amr_regrid.fpp 1703 +1703
src/simulation/m_amr_registers.fpp 1021 +1021
src/simulation/m_amr_restart.fpp 383 +383
src/post_process/m_data_input.f90 718 +310
src/simulation/m_ibm.fpp 1449 +200
src/simulation/m_sfc_partition.fpp 169 +169
src/simulation/m_phase_timing.fpp 167 +167
src/simulation/m_load_balance.fpp 164 +164
src/simulation/m_time_steppers.fpp 985 +157
src/post_process/m_data_output.fpp 1336 +146
src/simulation/m_amr_xchg_audit.fpp 142 +142
src/simulation/m_load_weight.fpp 121 +121
src/simulation/m_active_box.fpp 118 +118
src/common/m_box.fpp 71 +71
src/simulation/m_rhs.fpp 2022 +69
src/simulation/m_global_parameters.fpp 813 +66
src/simulation/m_start_up.fpp 1303 +59
src/simulation/m_rank_timing.fpp 50 +50
src/simulation/m_data_output.fpp 1363 +46
src/common/m_mpi_common.fpp 1515 +41
src/simulation/m_acoustic_src.fpp 557 +36
src/simulation/m_riemann_solvers.fpp 137 +25
src/common/m_phase_change.fpp 299 +22
src/common/include/2dHardcodedIC.fpp 453 +21
src/simulation/m_bubbles_EL.fpp 1676 +17
src/simulation/m_checker.fpp 84 +14
src/simulation/m_riemann_solver_lf.fpp 555 +14
src/common/include/macros.fpp 141 +13
src/simulation/m_weno.fpp 1350 +10
src/post_process/p_main.fpp 62 +7
src/simulation/m_hypoelastic.fpp 597 +7
src/simulation/m_igr.fpp 2342 +7
src/common/m_constants.fpp 91 +6
src/common/m_derived_types.fpp 443 +4
src/common/include/3dHardcodedIC.fpp 243 +3
src/simulation/m_riemann_solver_hll.fpp 578 -3
src/simulation/m_riemann_solver_hllc.fpp 1269 -3
src/simulation/m_riemann_solver_hlld.fpp 200 -3
src/simulation/m_surface_tension.fpp 243 +3
src/post_process/m_start_up.fpp 770 +2
src/simulation/m_cbc.fpp 1122 -2
src/common/m_boundary_common.fpp 449 +1
src/post_process/m_global_parameters.fpp 358 +1
src/simulation/m_bubbles_EE.fpp 317 +1
src/simulation/m_thinc.fpp 313 +1
Directory Lines Diff
common 9960 +182
simulation 39910 +11892
post_process 3803 +466
total 58169 +12540

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants